﻿//----------------------------------------------------------------
// <copyright file="GlobalListBuilder.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//----------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using MptCodeLibrary.Configuration;
using MptCodeLibrary.Exceptions;
using MptCodeLibrary.TeamFoundation;

namespace MptCodeLibrary
{
    /// <summary>
    /// The code in this class is responsible for maintaining the global lists
    /// for the MPT projects.  The global lists are used by work items to define parent/
    /// child relationships.
    /// </summary>
    public class GlobalListBuilder
    {
        private readonly IMptConfiguration mptConfiguration;
        private readonly ITeamFoundationFacade teamFoundationFacade;

        /// <summary>
        /// Initializes a new instance of the <see cref="GlobalListBuilder"/> class.
        /// </summary>
        /// <param name="mptConfiguration">The MPT configuration.</param>
        /// <param name="teamFoundationFacade">The team foundation facade.</param>
        public GlobalListBuilder(IMptConfiguration mptConfiguration, ITeamFoundationFacade teamFoundationFacade)
        {
            if (mptConfiguration == null)
                throw new ArgumentNullException("mptConfiguration");
            if (teamFoundationFacade == null)
                throw new ArgumentNullException("teamFoundationFacade");

            this.mptConfiguration = mptConfiguration;
            this.teamFoundationFacade = teamFoundationFacade;
        }

        /// <summary>
        /// Correctly formats the name of the global list for the project/work item type combination
        /// </summary>
        /// <param name="projectName">Name of the project.</param>
        /// <param name="workItemType">Type of the work item.</param>
        private string GlobalListName(string projectName, string workItemType)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0}_{1}_List", projectName,
                                 workItemType.Replace(" ", ""));
        }

        /// <summary>
        /// Correctly formats the ListItem values for the global list
        /// </summary>
        /// <param name="workItemId">The work item id.</param>
        /// <param name="workItemTitle">The work item title.</param>
        private string ListItemValue(int workItemId, string workItemTitle)
        {
            if (workItemId <= 0)
                throw new ArgumentException("Work item Id should be greater than 0", "workItemId");
            if (string.IsNullOrEmpty(workItemTitle))
                throw new ArgumentException("The work item title should not be empty", "workItemTitle");

            string newListItemText = string.Format(CultureInfo.InvariantCulture, "{0}: {1}", workItemId, workItemTitle);
            if (newListItemText.Length > 255)
                return newListItemText.Substring(0, 255);

            return newListItemText;
        }

        /// <summary>
        /// Deduces the type of the child work item.
        /// </summary>
        /// <param name="parentWorkItemType">Type of the parent work item.</param>
        /// <returns>Name of the child work item type</returns>
        private string DeriveChildWorkItemType(string parentWorkItemType)
        {
            string childWorkItemType;
            switch (parentWorkItemType)
            {
                case "Value Proposition":
                    childWorkItemType = "Feature Group";
                    break;
                case "Feature Group":
                    childWorkItemType = "Feature";
                    break;
                case "Feature":
                    childWorkItemType = "Deliverable";
                    break;
                case "Deliverable":
                    childWorkItemType = "Task";
                    break;
                default:
                    throw new MptException(string.Format(CultureInfo.InvariantCulture,
                                                         "An unknown work item type '{0}' was passed",
                                                         parentWorkItemType));
            }

            return childWorkItemType;
        }


        /// <summary>
        /// Figures out the name of the TFS server by looking at the DisplayUrl for the event.  If
        /// it matches a server name in the config files then it's good else throws an MptException.
        /// </summary>
        /// <param name="workItemChangedEvent">The work item changed event.</param>
        /// <returns>A TFS server name to use </returns>
        private string DeriveTfsServerName(XContainer workItemChangedEvent)
        {
            IEnumerable<XElement> displayUrlElement = workItemChangedEvent.Descendants("DisplayUrl");

            if (displayUrlElement.Count() == 0)
                throw new MptException(
                    "The work item changed event does't contain a DisplayUrl for figuring out the server it's from.  The XML: " +
                    workItemChangedEvent);

            Uri artifactMoniker = new Uri(displayUrlElement.First().Value);

            IEnumerable<TeamServerConfiguration> teamServers = from teamServer in mptConfiguration.TeamServers
                                                               where
                                                                   teamServer.ServerName.Equals(artifactMoniker.Host,
                                                                                                StringComparison.
                                                                                                    OrdinalIgnoreCase)
                                                                   ||
                                                                   artifactMoniker.AbsoluteUri.StartsWith(
                                                                       teamServer.ServerName,
                                                                       StringComparison.OrdinalIgnoreCase)
                                                               select teamServer;

            if (teamServers.Count() <= 0)
                throw new MptException("Could not determine the TFS server name to use.  The DisplayUrl is: " + artifactMoniker.AbsoluteUri);

            return artifactMoniker.Scheme + "://" + artifactMoniker.Authority;
        }

        /// <summary>
        /// Updates the global lists for all of the defined projects on the tfs servers defined by the configuration file.
        /// </summary>
        /// <remarks>
        /// The code looks for work items that may have been updated in the last 48 hours and does these as opposed to
        /// a clean rebuild of all lists for all projects.  This could enable the rebuild process to be less resource consuming
        /// and potentially let the process run more frequently.
        /// </remarks>
        public void RebuildGlobalListsForProjects()
        {
            foreach (TeamServerConfiguration teamServerConfiguration in mptConfiguration.TeamServers)
            {
                IEnumerable<WorkItemSummary> changedWorkItems = teamFoundationFacade.ChangedWorkItems(teamServerConfiguration.ServerName,
                                                                                               mptConfiguration.TeamProjects);

                IEnumerable<string> projectsThatAreAffected = changedWorkItems.Select(p => p.ProjectName).Distinct();
                foreach (string projectName in projectsThatAreAffected)
                {
                    UpdateGlobalListsForProject(teamServerConfiguration.ServerName, projectName, changedWorkItems);
                }
            }
        }

        private void UpdateGlobalListsForProject(string tfsServerName, string projectName, IEnumerable<WorkItemSummary> changedWorkItems)
        {
            var workItemTypesForProject = (from workItem in changedWorkItems
                                       where
                                           workItem.ProjectName.Equals(projectName, StringComparison.OrdinalIgnoreCase)
                                       select workItem).Select(p => p.WorkItemType).Distinct();

            foreach (string workItemSummaryType in workItemTypesForProject)
            {
                IEnumerable<WorkItemSummary> workItemsForProjectOfType =
                    teamFoundationFacade.GetWorkItemsForProjectOfType(tfsServerName, projectName,
                                                                      workItemSummaryType);

                XElement globalList = new XElement("GLOBALLIST",
                                                            new XAttribute("name",
                                                                           GlobalListName(projectName, workItemSummaryType)),
                                                            from workitem in workItemsForProjectOfType
                                                            select
                                                                new XElement("LISTITEM",
                                                                             new XAttribute("value",
                                                                                            ListItemValue(
                                                                                                workitem.WorkItemId,
                                                                                                workitem.Title))));
                XDocument globalLists = new XDocument(new XElement("GLOBALLISTS", globalList));

                teamFoundationFacade.SetGlobalList(tfsServerName, globalLists.ToString());

                // Need to set the global list first else the WIT update process will fail
                // Update all WIT just in case the WIT didn't get updated during the TFS Event or 
                // the global list already existed.
                UpdateWorkItemTypeWithGlobalList(tfsServerName, projectName, workItemSummaryType);
            }
        }

        /// <summary>
        /// Updates the global list, for the server for the work item type that has caused the change.
        /// </summary>
        /// <param name="eventXml">The workitem changed XML, which is received from TFS eventing.</param>
        public void UpdateGlobalList(string eventXml)
        {
            XDocument changeEvent = XDocument.Parse(eventXml);

            string changeType = (from element in changeEvent.Descendants("ChangeType")
                                 select element.Value).First();

            string newWorkItemTitle = (from element in changeEvent.Descendants("WorkItemTitle")
                                       select element.Value).First();

            // there has to be a System.Title in the work item changed event XML.  It's part of the requird XML schema
            string existingWorkItemTitle = (from element in changeEvent.Descendants("Field")
                                            where string.Equals((string)element.Element("ReferenceName"), "System.Title",
                                                              StringComparison.OrdinalIgnoreCase)
                                            select (string)element.Element("OldValue")).First();

            if (!ChangeNeedsGlobalListUpdate(changeType, newWorkItemTitle, existingWorkItemTitle))
                return;

            string projectName = (from element in changeEvent.Descendants("PortfolioProject")
                                  select element.Value).First();

            string tfsServerName = DeriveTfsServerName(changeEvent);

            // there has to be a System.Id in the work item changed event XML.  It's part of the requird XML schema
            int workItemId = (from element in changeEvent.Descendants("Field")
                              where string.Equals((string)element.Element("ReferenceName"), "System.Id", StringComparison.OrdinalIgnoreCase)
                              select (int)element.Element("NewValue")).First();

            string workItemType = WorkItemTypeForEvent(tfsServerName, changeEvent, workItemId);
            
            UpdateListItemInGlobalList(tfsServerName, projectName, workItemId, newWorkItemTitle, existingWorkItemTitle,
                                       workItemType);
        }
        
        private string WorkItemTypeForEvent(string tfsServerName, XContainer changeEvent, int workItemId)
        {
            IEnumerable<string> workItemTypeCollection = from element in changeEvent.Descendants("Field")
                                                         where
                                                             string.Equals((string) element.Element("ReferenceName"),
                                                                           "Microsoft.VSTS.Common.WITCategory",
                                                                           StringComparison.OrdinalIgnoreCase)
                                                         select (string) element.Element("NewValue");

            string workItemType;
            if (workItemTypeCollection.Count() > 0)
                workItemType = workItemTypeCollection.First();
            else
            {
                WorkItemSummary workItemSummary = teamFoundationFacade.GetWorkItem(tfsServerName, workItemId);
                workItemType = workItemSummary.WorkItemType;
            }

            return workItemType;
        }

        private static bool ChangeNeedsGlobalListUpdate(string changeType, string newWorkItemTitle, string existingWorkItemTitle)
        {
            bool thisIsAChangeEvent = String.IsNullOrEmpty(changeType) ||
                                      changeType.Equals("Change", StringComparison.OrdinalIgnoreCase);

            return !(thisIsAChangeEvent && !TheTitleOfTheWorkItemHasChanged(newWorkItemTitle, existingWorkItemTitle));
        }

        private static bool TheTitleOfTheWorkItemHasChanged(string newWorkItemTitle, string existingWorkItemTitle)
        {
            // The title is empty when the event is for a new work item, in which case the code shouldn't follow
            // the "title has changed" logic
            return !string.IsNullOrEmpty(existingWorkItemTitle) &&
                   !newWorkItemTitle.Equals(existingWorkItemTitle, StringComparison.OrdinalIgnoreCase);
        }
        
        private void UpdateListItemInGlobalList(string tfsServerName, string projectName, int workItemId, string newWorkItemTitle, 
            string existingWorkItemTitle, string workItemType)
        {
            bool updateExistingWorkItemsWithTitleChange = TheTitleOfTheWorkItemHasChanged(newWorkItemTitle, existingWorkItemTitle);

            XDocument globalList = XDocument.Parse(teamFoundationFacade.GetGlobalLists(tfsServerName));
            string globalListName = GlobalListName(projectName, workItemType);

            IEnumerable<XElement> existingLists = from element in globalList.Descendants("GLOBALLIST")
                                                  where string.Equals(element.Attribute("name").Value,
                                                                    globalListName,
                                                                    StringComparison.OrdinalIgnoreCase)
                                                  select element;
            
            XElement existingList = new XElement("LISTITEM");
            if (existingLists.Count() > 0)
            {
                existingList = existingLists.First();
            }

            XElement listWithoutOldTitle = new XElement("GLOBALLIST", 
                                               new XAttribute("name", globalListName),
                                               from element in existingList.Descendants("LISTITEM")
                                               where 
                                                    !ListItemIsOldWorkItemTitleWhenTheTitleHasChanged(updateExistingWorkItemsWithTitleChange, element, workItemId, existingWorkItemTitle)
                                                    && !ListItemIsListItemBeingAdded(element, workItemId, newWorkItemTitle)
                                               select element);

            listWithoutOldTitle.Add(new XElement("LISTITEM", new XAttribute("value", ListItemValue(workItemId, newWorkItemTitle))));

            XDocument document = new XDocument(new XElement("GLOBALLISTS", listWithoutOldTitle));

            teamFoundationFacade.SetGlobalList(tfsServerName, document.ToString());

            if (updateExistingWorkItemsWithTitleChange) 
            {
                // the title has changed, need to wait for the global list to change before updating existing work items
                teamFoundationFacade.UpdateExistingWorkItemsWithNewParent(tfsServerName, projectName,
                                                                          ListItemValue(workItemId,existingWorkItemTitle),
                                                                          ListItemValue(workItemId, newWorkItemTitle));
            }

            // can't update the work item type till after the global list has been created
            UpdateWorkItemTypeWithGlobalList(tfsServerName, projectName, workItemType);
        }

        private bool ListItemIsOldWorkItemTitleWhenTheTitleHasChanged(bool updateExistingWorkItemsWithTitleChange, XElement element, int workItemId, string existingWorkItemTitle)
        {
            return !(!updateExistingWorkItemsWithTitleChange
                   || !string.Equals(element.Attribute("value").Value, ListItemValue(workItemId, existingWorkItemTitle), StringComparison.OrdinalIgnoreCase));
        }

        private bool ListItemIsListItemBeingAdded(XElement element, int workItemId, string newWorkItemTitle)
        {
            return string.Equals(element.Attribute("value").Value, ListItemValue(workItemId, newWorkItemTitle), StringComparison.OrdinalIgnoreCase);
        }

        private void UpdateWorkItemTypeWithGlobalList(string tfsServerName, string projectName, string workItemType)
        {
            XDocument wit =
                XDocument.Parse(teamFoundationFacade.GetWorkItemTypeForProject(tfsServerName, projectName,
                                                                               DeriveChildWorkItemType(workItemType)));

           var parentShortNameField = (from field in
                                       ((from workitemtype in
                                             ((from workitemtype in wit.Descendants("WORKITEMTYPE")
                                               select workitemtype).First()).Descendants("FIELDS")
                                         select workitemtype).First()).Descendants("FIELD")
                                   where
                                       field.Attribute("refname").Value.Equals("Microsoft.VSTS.Common.ParentShortName")
                                   select field).First();

            var allowedValues = parentShortNameField.Descendants("ALLOWEDVALUES").First();

            string globalListName = GlobalListName(projectName, workItemType);

            // if the WIT has already been updated don't do it again.
            if (allowedValues.Elements("GLOBALLIST").Count() > 0)
            {
                XElement existingGlobalList = allowedValues.Elements("GLOBALLIST").First();
                XAttribute nameAttribute = existingGlobalList.Attribute("name");

                if (nameAttribute != null
                    && nameAttribute.Value.Equals(globalListName, StringComparison.OrdinalIgnoreCase))
                    return;
            }

            XElement newGlobalList = new XElement("GLOBALLIST",
                                                  new XAttribute("name", globalListName));
            allowedValues.ReplaceNodes(newGlobalList);

            teamFoundationFacade.UpdateWorkItemTypeForProject(tfsServerName, projectName, wit.ToString());
        }
    }
}
